package org.originit.analyze.controller;

import cn.hutool.http.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.originit.analyze.consts.BookConst;
import org.originit.analyze.entity.Book;
import org.originit.analyze.entity.BookDetails;
import org.originit.analyze.entity.Breadcrumb;
import org.originit.analyze.entity.Category;
import org.originit.analyze.util.ResourceUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@Controller
@Slf4j
public class BookController {


    @Value("${outside.resource.path}")
    private String path;

    @Value("${outside.resource.prefix}")
    private String resourcePrefix;

    @Value("${server.port}")
    private int port;

    @GetMapping("/single")
    public String single(Model model, @RequestParam(required = false) String category,
                         @RequestParam(required = false) String bookName,
                         @RequestParam String fileType,
                         @RequestParam(required = false) String link) throws UnsupportedEncodingException {
        model.addAttribute("category", category);
        model.addAttribute("bookName", bookName);
        link = link.replaceAll("\\\\", URLEncoder.encode("\\", "UTF-8"));
        if (BookConst.SUFFIX_PDF.equals(fileType)) {
            return "redirect:" + getPdfUrl(link);
        }
        String url = getUrl(link);
        model.addAttribute("content", HttpUtil.get(url));
        return "book-single-page";
    }

    private String getUrl(String link) {
        return "http://localhost:" + port + link;
    }

    private String getPdfUrl(String link) {
        return "/js/pdf.js/web/viewer.html?file=" + encodeArg(decodeArg(link));
    }

    /**
     * 查询目录，若有分类则查看分类下的目录
     *
     * @param category 书的分类
     * @throws IOException
     */
    @RequestMapping("/")
    public String index(Model model, @RequestParam(required = false) String category) throws IOException {
        List<Category> categoryList = handleCategories();
        if (category == null && !categoryList.isEmpty()) {
            category = categoryList.get(0).getName();
        }
        List<Book> books = Collections.EMPTY_LIST;
        if (category != null) {
            books = handleBooks(category);
        }
        model.addAttribute("category", category);
        model.addAttribute("categories", categoryList);
        model.addAttribute("books", books);
        return "index";
    }

    /**
     * 处理url的中文
     *
     * @return 编码后的url
     */
    private String encodeArg(String arg) {
        try {
            return URLEncoder.encode(arg,"utf8").replaceAll("\\+", "%20");
        } catch (UnsupportedEncodingException e) {
            log.error("encode error for {}", arg, e);
            throw new RuntimeException(e);
        }
    }

    private String decodeArg(String arg) {
        try {
            return URLDecoder.decode(arg.replaceAll("%20", "+"), "utf8");
        } catch (UnsupportedEncodingException e) {
            log.error("decode error for {}", arg, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 处理分类下的所有书籍
     *
     * @param category 分类名称
     * @return 该分类下的书籍的名称分类及详情url
     * @throws IOException
     */
    private List<Book> handleBooks(String category) throws IOException {
        List<Book> books = Collections.EMPTY_LIST;
        Resource[] booksRes = ResourceUtil.loadResources(path + File.separator + category + "/*");
        if (booksRes.length > 0) {
            books = new ArrayList<>(booksRes.length);
            for (Resource bookRes : booksRes) {
                if (!bookRes.isFile() || bookRes.getFile().isFile()) {
                    continue;
                }
                Book book = new Book();
                book.setBookName(bookRes.getFilename());
                book.setCategory(category);
                book.setUrl("/book?category=" + encodeArg(category) + "&bookName=" + encodeArg(book.getBookName()));
                books.add(book);
            }
        }
        return books;
    }

    /**
     * 处理配置的outside.resource.path下的分类列表
     *
     * @return 分类的相关信息
     * @throws IOException
     */
    private List<Category> handleCategories() throws IOException {
        Resource[] categoriesRes = ResourceUtil.loadResources(path + "/*");
        List<Category> categoryList = Collections.EMPTY_LIST;
        if (categoriesRes.length > 0) {
            categoryList = new ArrayList<>(categoriesRes.length);
            for (Resource resource : categoriesRes) {
                // 分类应该是文件夹类型
                if (!resource.isFile() || resource.getFile().isFile()) {
                    continue;
                }
                final Category c = new Category();
                c.setName(resource.getFilename());
                c.setUrl("/?category=" + encodeArg(c.getName()));
                categoryList.add(c);
            }
        }
        return categoryList;
    }


    /**
     * 查看书的所有内容
     *
     * @param category 书分类
     * @param bookName 书名或者所在文件夹
     * @param link     当前是哪一章
     */
    @RequestMapping("/book")
    public String book(Model model, @RequestParam String category, @RequestParam String bookName, @RequestParam(defaultValue = "") String selectedPath,
                       @RequestParam(required = false) String link, @RequestParam(required = false) String type) throws IOException {
        String suffix;
        if (type == null || !BookConst.SUFFIX_ALL.contains(type)) {
            suffix = BookConst.SUFFIX_HTML;
        } else {
            suffix = type;
        }

        StringBuilder sb = new StringBuilder();
        String bookPath = sb.append(path).append(File.separator)
            .append(category).append(File.separator)
            .append(bookName).toString();
        // 路径校验
        bookPath = new File(bookPath).getAbsolutePath();
        String curPath = sb.append(selectedPath).toString();
        // 当前文件目录级别
        String contextPath;
        if (isFile(curPath)) {
            contextPath = new File(curPath).getParent();
        } else {
            contextPath = curPath;
        }
        List<BookDetails> booksDetails = new ArrayList<>();
        File file = new File(contextPath);
        File[] subFiles = file.listFiles();
        if (file.exists()) {
            // 遍历文件内容
            for (File resource : Objects.requireNonNull(subFiles)) {
                BookDetails pageInfo = getDetails(category, bookName, resource, suffix, bookPath);
                if (pageInfo == null)
                    continue;
                booksDetails.add(pageInfo);
            }
        }
        booksDetails.sort(Comparator.comparing(BookDetails::getName));
        Optional<BookDetails> detailsOptional = booksDetails.stream().filter(book -> book.getForwardUrl().equals(link)).findFirst();
        File lastLevel = getLastLevel(booksDetails, bookPath, curPath);
        if (lastLevel != null) {
            BookDetails details = getDetails(category, bookName, lastLevel, suffix, bookPath);
            if (details != null) {
                details.setName("上一级");
                booksDetails.add(0, details);
            }
        }
        model.addAttribute("fileType", suffix);
        model.addAttribute("fileTypes",BookConst.SUFFIX_ALL);
        model.addAttribute("booksDetails", booksDetails);
        model.addAttribute("link", link);
        model.addAttribute("curDetail", detailsOptional.orElse(null));
        model.addAttribute("bookName", bookName);
        model.addAttribute("filePaths", getBreadcrumbs(category, bookName, suffix, curPath, bookPath));
        return "book-details";
    }

    private List<Breadcrumb> getBreadcrumbs(String category, String bookName, String suffix, String curPath,
        String bookPath) {
        List<Breadcrumb> breadcrumbs = new ArrayList<>();
        breadcrumbs.add(new Breadcrumb(bookName, getUrl(category, bookName, suffix, "")));
        String[] splited = splitPathWithSeparator(curPath.substring(bookPath.length()));
        StringBuilder path = new StringBuilder();
        for (String s : splited) {
            if (s.trim().isEmpty()) {
                continue;
            }
            path.append(File.separator).append(s);
            Breadcrumb breadcrumb = new Breadcrumb();
            breadcrumb.setName(s);
            breadcrumb.setUrl(getUrl(category, bookName, suffix, path.toString()));
            breadcrumbs.add(breadcrumb);
        }
        return breadcrumbs;
    }

    private BookDetails getDetails(String category, String bookName, File resource, String suffix, String bookPath) {
        BookDetails pageInfo = null;
        if (resource.isDirectory()) {
            pageInfo = new BookDetails();
            pageInfo.setName(resource.getName());
            pageInfo.setDir(true);
        } else {
            if (!resource.getName().endsWith(suffix)) {
                return null;
            }
            pageInfo = new BookDetails();
            pageInfo.setName(resource.getName());
        }
        pageInfo.setFilePath(resource.getAbsolutePath().substring(bookPath.length()));
        String forwardUrl = resourcePrefix + File.separator + encodeArg(category) + File.separator + encodeArg(bookName) + File.separator + encodePath(pageInfo.getFilePath());
        pageInfo.setForwardUrl(forwardUrl);
        pageInfo.setUrl(getUrl(category, bookName, suffix, pageInfo.getFilePath()));
        return pageInfo;
    }

    private String getUrl(String category, String bookName, String suffix, String filePath) {
        String forwardUrl = resourcePrefix + File.separator + encodeArg(category) + File.separator + encodeArg(bookName) + File.separator + encodePath(filePath);
        return "/book?category=" + encodeArg(category) + "&bookName=" + encodeArg(bookName) + "&type=" + suffix + "&link=" + encodeArg(forwardUrl) + "&selectedPath=" + encodeArg(filePath);
    }

    private File getLastLevel(List<BookDetails> details, String bookPath, String curPath) {
        if (details == null) {
            return null;
        }
        if (StringUtils.isEmpty(bookPath) || StringUtils.isEmpty(curPath) || bookPath.equals(curPath)) {
            return null;
        }
        File bookPathFile = new File(bookPath);
        File curPathFile = new File(curPath);
        if (!bookPathFile.exists() || !curPathFile.exists()) {
            return null;
        }
        bookPath = bookPathFile.getAbsolutePath();
        curPath = curPathFile.getAbsolutePath();
        String filePath = curPath.substring(bookPath.length());
        if (filePath.contains(File.separator)) {
            return new File(new File(curPath).getParent());
        }
        return null;
    }

    private String encodePath(String path) {
        return Arrays.stream(splitPathWithSeparator(path))
            .map(String::trim)
            .filter(s -> !s.isEmpty())
            .map(this::encodeArg)
            .collect(Collectors.joining(File.separator));
    }

    private static String[] splitPathWithSeparator(String path) {
        String separator = File.separator;
        // on windows separator is \ so we need to replace it to \\ to avoid regex error
        // regex \ is special char
        if (Objects.equals(separator, "\\")) {
            separator = "\\\\";
        }
        return path.split(separator);
    }

    private boolean isFile(String path) {
        return !new File(path).isDirectory();
    }

    private void resolveResources(String pattern, Consumer<Resource> resourceConsumer) {
        resolveResources(pattern, resourceConsumer, null);
    }

    private void resolveResources(String pattern, Consumer<Resource> resourceConsumer, Predicate<Resource> resourcePredicate) {
        Assert.notNull(resourceConsumer, "请传入consumer进行处理");
        Resource[] categoriesRes = ResourceUtil.loadResources(pattern);
        for (Resource resource : categoriesRes) {
            if (resourcePredicate != null && !resourcePredicate.test(resource)) {
                continue;
            }
            resourceConsumer.accept(resource);
        }
    }

    Predicate<Resource> dirPredicate = resource -> {
        try {
            return resource.isFile() && resource.getFile().isDirectory();
        } catch (IOException e) {
            return false;
        }
    };

    Predicate<Resource> filePredicate = resource -> {
        try {
            return resource.isFile() && resource.getFile().isFile();
        } catch (IOException e) {
            return false;
        }
    };

    @RequestMapping("/formatFiles")
    @ResponseBody
    public String formatFiles(@RequestParam(required = false) String category, @RequestParam(required = false) String bookName, @RequestParam String originStr, @RequestParam(defaultValue = "") String replacement, @RequestParam(defaultValue = "true") boolean includeDir) {
        Function<Resource, File> replaceFunc = res -> {
            try {
                final File dest = new File(Objects.requireNonNull(res.getFile().getAbsolutePath()).replace(originStr, replacement));
                boolean success = res.getFile().renameTo(dest);
                if (success) {
                    return dest;
                } else {
                    return res.getFile();
                }
            } catch (IOException e) {
                return null;
            }
        };
        if (category == null) {
            // 修改所有的书
            resolveResources(path + "/*", resource -> {
                // 重命名
                File file = replaceFunc.apply(resource);
                if (file == null) {
                    return;
                }
                // 递归处理分类下的书籍
                replaceBooksName(file.getName(), replaceFunc);
            }, dirPredicate);
        } else {
            if (bookName == null) {
                // 修改具体某本书
                replaceDetailsName(category, bookName, replaceFunc);
            } else {
                // 修改指定分类的书
                // 递归处理分类下的书籍
                replaceBooksName(category, replaceFunc);
            }
        }
        return String.format("category is %s and originStr is %s",category,originStr);
    }

    private void replaceBooksName(String category, Function<Resource, File> replaceFunc) {
        resolveResources(path + File.separator + category + "/*", bookRes -> {
            final File bookFile = replaceFunc.apply(bookRes);
            if (bookFile == null) {
                return;
            }
            replaceDetailsName(category, bookFile.getName(), replaceFunc);
        }, dirPredicate);
    }

    private void replaceDetailsName(String category, String bookName, Function<Resource, File> replaceFunc) {
        resolveResources(path + File.separator + category + File.separator + bookName + "/*", replaceFunc::apply, filePredicate);
    }

}
